﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;

namespace CHIP8Emulator {
    public partial class DisassemblyCodeControl : UserControl {
        private const int EM_GET_SCROLL_POS = WM_USER + 221;
        private const int EM_SET_SCROLL_POS = WM_USER + 222;
        private const string NOP_BYTE_CODE = "0000h";
        private const int WM_USER = 0x400;
        private bool[] _changedLines = new bool[0];
        private Logger _logger = new ConsoleLogger();
        private int _prevCursorPositionLine = 1;

        [DllImport("user32.dll")]
        private static extern void SendMessage(IntPtr hWnd, int msg, IntPtr wParam, ref Point lParam);

        public DisassemblyCodeControl() {
            InitializeComponent();
            _mnemonicTextBox.CursorPositionChanged += _mnemonicTextBox_CursorPositionChanged;
            _mnemonicTextBox.KeyDown += _mnemonicTextBox_CursorPositionChanged;
        }

        private int CodeLinesCount {
            get { return Math.Max(_mnemonicTextBox.Lines.Length, 1); }
        }

        private void _mnemonicTextBox_CursorPositionChanged(object sender, EventArgs e) {
            if (_mnemonicTextBox.Lines.Length != _addressTextBox.Lines.Length)
                ResizeLinesArrays();
//            if (_prevCursorPositionLine != CursorPosition.Line(_mnemonicTextBox, _mnemonicTextBox.SelectionStart))
//                CheckCodeLine(_prevCursorPositionLine);
        }

        private void CheckCodeLine(int lineIdx) {
            if (lineIdx > _mnemonicTextBox.Lines.Length || !CodeLineChanged(lineIdx - 1))
                return;
            ChangeByteCodeLine(GetByteCode(_mnemonicTextBox.Lines[lineIdx - 1], lineIdx), lineIdx);
            _changedLines[lineIdx - 1] = false;
        }

        private void ChangeByteCodeLine(string byteCode, int idx) {
            _byteCodeTextBox.Text = JoinLines(_byteCodeTextBox.Lines, 0, idx - 1) +
                                    (idx > 1 ? Environment.NewLine : string.Empty) + byteCode +
                                    JoinLines(_byteCodeTextBox.Lines, idx);
        }

        private static string JoinLines(string[] lines, int startIdx) {
            return JoinLines(lines, startIdx, lines.Length - startIdx);
        }

        private string GetByteCode(string codeLine, int lineIdx) {
            string line = codeLine.ToUpper().Trim();
            int indexOfComment = codeLine.IndexOf(';');
            if (indexOfComment >= 0)
                line = line.Remove(indexOfComment);
            if (string.IsNullOrEmpty(line.Trim())) {
                return NOP_BYTE_CODE;
            }
            Match match = new Regex(@"\A[A-Z]+\s*").Match(line);
            if (!match.Success) {
                _logger.Log(LogMessageType.Error, "Unrecognize command '{0}' at address {1}", line,
                            _addressTextBox.Lines[lineIdx - 1]);
                return NOP_BYTE_CODE;
            }
            string argsString = line.Remove(0, match.Value.Length).Trim();
            Match argsMatch = new Regex(@"\A\w+(\s*,\s*\w+)*").Match(argsString);
            if (argsMatch.Value.Length != argsString.Length) {
                _logger.Log(LogMessageType.Error, "Syntax error at address {0}", _addressTextBox.Lines[lineIdx - 1]);
                return NOP_BYTE_CODE;
            }
            string[] args = SplitString(argsString, ',');
            try {
                CommandCode commandCode = CommandCode.Create(match.Value.Trim(), args);
            }
            catch (Exception ex) {
                _logger.Log(LogMessageType.Error, ex.Message);
            }
            return NOP_BYTE_CODE;
        }

        private static string[] SplitString(string str, char separator) {
            if (string.IsNullOrEmpty(str))
                return new string[0];
            return str.Split(separator);
        }

        private bool CodeLineChanged(int idx) {
            return _changedLines[idx];
        }

        private void ResizeLinesArrays() {
            string[] addressLines = _addressTextBox.Lines;
            Array.Resize(ref addressLines, CodeLinesCount);
            _addressTextBox.Text = JoinLines(addressLines);
            _addressTextBox.Text +=
                (CHIP8Device.PC_INIT_VALUE + (CodeLinesCount - 1) * 2).ToString("X" + CHIP8Device.OPCODE_SIZE * 2) + "h";
            string[] byteCodeLines = _byteCodeTextBox.Lines;
            Array.Resize(ref byteCodeLines, CodeLinesCount);
            _byteCodeTextBox.Text = JoinLines(byteCodeLines);
            Array.Resize(ref _changedLines, CodeLinesCount + 2);
        }

        private static string JoinLines(string[] lines) {
            return JoinLines(lines, 0, lines.Length);
        }

        private static string JoinLines(string[] lines, int startIdx, int length) {
            StringBuilder builder = new StringBuilder();
            for (int idx = startIdx; idx < length - 1; idx++)
                builder.AppendLine(lines[idx] ?? string.Empty);
            if (startIdx + length - 1 < lines.Length && length > 0)
                builder.Append(lines[startIdx + length - 1]);
            return builder.ToString();
        }

        private void _mnemonicTextBox_VScroll(object sender, EventArgs e) {
            Point pos = new Point();
            SendMessage(_mnemonicTextBox.Handle, EM_GET_SCROLL_POS, IntPtr.Zero, ref pos);
            SendMessage(_addressTextBox.Handle, EM_SET_SCROLL_POS, IntPtr.Zero, ref pos);
            SendMessage(_byteCodeTextBox.Handle, EM_SET_SCROLL_POS, IntPtr.Zero, ref pos);
        }

        private void _mnemonicTextBox_TextChanged(object sender, EventArgs e) {
            _changedLines[CursorPosition.Line(_mnemonicTextBox, _mnemonicTextBox.SelectionStart) - 1] = true;
        }

        private void _mnemonicTextBox_KeyPress(object sender, KeyPressEventArgs e) {
            if (e.KeyChar == 13) {
                int leadingSpaces =
                    CountLeadingSpaces(
                        _mnemonicTextBox.Lines[
                            CursorPosition.Line(_mnemonicTextBox, _mnemonicTextBox.SelectionStart) - 2]);
                var builder = new StringBuilder();
                for (int i = 0; i < leadingSpaces; i++)
                    builder.Append(" ");
                _mnemonicTextBox.SelectedText = builder.ToString();
            }
        }

        private static int CountLeadingSpaces(string str) {
            for (int i = 0; i < str.Length; i++)
                if (str[i] != ' ')
                    return i;
            return str.Length;
        }

        public void InitBy(Logger logger) {
            _logger = logger;
        }
    }

    internal class CommandCode {
        private static readonly Dictionary<int, List<string>> OPCODE_ARGS_COUNT =
            new Dictionary<int, List<string>>
                {
                    {
                        0, new List<string>
                               {
                                   "CLS", // clear screen
                                   "RET", // return from subroutine
                               }
                        },
                    {
                        1, new List<string>
                               {
                                   "CALL", // call subroutine
                                   "SYS", // call program
                                   "JP", // jump to address
                                   "SHR", // shift VX right by one bit
                                   "SHL", // shift VX left by one bit
                                   "SKP", // skip next instruction if key in VX is pressed
                                   "SKNP", // skip next instruction if key in VX is not pressed
                               }
                        },
                    {
                        2, new List<string>
                               {
                                   "ADD",
                                   "AND",
                                   "DRW",
                                   "JP",
                                   "LD",
                                   "OR",
                                   "RND",
                                   "SE",
                                   "SNE",
                                   "SUB",
                                   "SUNB",
                                   "XOR",
                               }
                        },
                };

        public static CommandCode Create(string opcode, string[] args) {
            if (!OPCODE_ARGS_COUNT.ContainsKey(args.Length) || !OPCODE_ARGS_COUNT[args.Length].Contains(opcode))
                throw new Exception(string.Format("Command {0} expects {1} arguments,  but {2} supplied.", opcode, GetArgsCount(opcode),
                                                  args.Length));
            return null;
        }

        private static int GetArgsCount(string opcode) {
            foreach (KeyValuePair<int, List<string>> pair in OPCODE_ARGS_COUNT)
                if (pair.Value.Contains(opcode))
                    return pair.Key;
            throw new Exception(string.Format("Unrecognized command {0}", opcode));
        }
    }

    public class RicherTextBox : RichTextBox {
        public int CurrentColumn {
            get { return CursorPosition.Column(this, SelectionStart); }
        }

        public int CurrentLine {
            get { return CursorPosition.Line(this, SelectionStart); }
        }

        public int CurrentPosition {
            get { return SelectionStart; }
        }

        public int SelectionEnd {
            get { return SelectionStart + SelectionLength; }
        }

        public event EventHandler CursorPositionChanged;

        protected virtual void OnCursorPositionChanged(EventArgs e) {
            if (CursorPositionChanged != null)
                CursorPositionChanged(this, e);
        }

        protected override void OnSelectionChanged(EventArgs e) {
            if (SelectionLength == 0)
                OnCursorPositionChanged(e);
            else
                base.OnSelectionChanged(e);
        }
    }

    internal class CursorPosition {
        [DllImport("user32")]
        public static extern int GetCaretPos(ref Point lpPoint);

        private static int GetCorrection(RichTextBox e, int index) {
            Point pt1 = Point.Empty;
            GetCaretPos(ref pt1);
            Point pt2 = e.GetPositionFromCharIndex(index);

            if (pt1 != pt2)
                return 1;
            return 0;
        }

        public static int Line(RichTextBox richTextBox, int index) {
            return richTextBox.GetLineFromCharIndex(index) - GetCorrection(richTextBox, index) + 1;
        }

        public static int Column(RichTextBox richTextBox, int idx) {
            int correction = GetCorrection(richTextBox, idx);
            Point p = richTextBox.GetPositionFromCharIndex(idx - correction);

            if (p.X == 1)
                return 1;

            p.X = 0;
            return idx - richTextBox.GetCharIndexFromPosition(p) + 1;
        }
    }
}